<!doctype html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>乳腺癌术后上肢康复评估与训练平台（原型）</title>
  <style>
    :root { --bg:#f7f9fc; --text:#111827; --card-bg:#fff; --border:#e5e7eb; --muted:#6b7280; --primary:#2563eb; --header-bg:#1f2937; --header-text:#fff; }
    .dark { --bg:#0f172a; --text:#e5e7eb; --card-bg:#111827; --border:#334155; --muted:#94a3b8; --primary:#4f46e5; --header-bg:#0b1220; --header-text:#e5e7eb; }
    body { font-family: -apple-system, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', sans-serif; margin: 0; padding: 0; background: var(--bg); color: var(--text); }
    header { background: var(--header-bg); color: var(--header-text); padding: 16px 20px; display:flex; align-items:center; justify-content:space-between; }
    header .brand { display:flex; gap:10px; align-items:center; }
    header .actions { display:flex; gap:8px; }
    main { padding: 16px; max-width: 1080px; margin: 0 auto; }
    .grid { display: grid; grid-template-columns: 2fr 1fr; gap: 16px; }
    .card { background: var(--card-bg); border-radius: 12px; border: 1px solid var(--border); box-shadow: 0 1px 2px rgba(0,0,0,0.04); padding: 14px; transition: transform .12s ease, box-shadow .12s ease; }
    .card:hover { transform: translateY(-1px); box-shadow: 0 8px 16px rgba(0,0,0,0.10); }
    .card h3 { margin: 0 0 8px; font-size: 18px; color: var(--text); }
    .actions { display: flex; gap: 8px; margin-top: 10px; }
    button { padding: 8px 12px; border: 0; border-radius: 8px; background: var(--primary); color: var(--header-text); cursor: pointer; transition: filter .12s ease, opacity .12s ease; }
    button:hover { filter: brightness(1.05); }
    button:disabled { opacity: .6; cursor: not-allowed; }
    button.secondary { background: var(--muted); color: var(--header-text); }
    button.ghost { background: transparent; color: var(--header-text); border: 1px solid rgba(255,255,255,0.3); }
    a.ghost { background: transparent; color: var(--header-text); border: 1px solid rgba(255,255,255,0.3); padding:8px 12px; border-radius:8px; text-decoration:none; }
    a.ghost:hover { border-color: rgba(255,255,255,0.5); }
    pre { white-space: pre-wrap; word-wrap: break-word; }
    table { width:100%; border-collapse: collapse; font-size: 13px; }
    thead th { font-weight: 600; }
    th, td { border-bottom: 1px solid var(--border); padding: 6px 8px; text-align:left; }
    tbody tr:hover { background: rgba(99,102,241,0.06); }
    .stat-row { display:flex; gap:12px; }
    .stat { background: var(--card-bg); border:1px solid var(--border); border-radius:10px; padding:8px 10px; flex:1; }
    .stat .label { color: var(--muted); font-size:12px; }
    .stat .value { color: var(--text); font-size:18px; font-weight:600; }
    .field { display:flex; flex-direction:column; gap:4px; margin-bottom:8px; }
    .field input, .field select, .field textarea { border:1px solid var(--border); border-radius:8px; padding:8px; font-size:14px; background: var(--card-bg); color: var(--text); }
    .toast { position:fixed; right:20px; bottom:20px; background: var(--text); color: var(--card-bg); padding:10px 14px; border-radius:10px; opacity:0; transform:translateY(10px); transition:all .2s; }
    .toast.show { opacity:1; transform:translateY(0); }
  </style>
  <!-- echarts disabled: 使用文本降级展示，避免CDN超时错误 -->
</head>
<body>
<header>
  <div class="brand">
    <h2>乳腺癌术后上肢康复评估与训练平台（原型）</h2>
  </div>
  <div class="actions">
    <button class="ghost" id="simulate">模拟一次数据</button>
    <button class="ghost" id="refresh">刷新</button>
    <button class="ghost" id="toggle-theme">切换主题</button>
    <a class="ghost" href="/doctor">医生端</a>
    <a class="ghost" href="/">主页</a>
  </div>
</header>
<main>
  <div class="grid">
    <div class="card">
      <h3>功能评分趋势（乳腺癌术后）</h3>
      <div id="chart" style="width:100%;height:320px"></div>
      <pre id="chart-fallback" style="font-size:13px;color:#334155"></pre>
    </div>
    <div class="card">
      <h3>关键统计</h3>
      <div class="stat-row">
        <div class="stat"><div class="label">评估次数</div><div id="stat-count" class="value">-</div></div>
        <div class="stat"><div class="label">平均评分</div><div id="stat-avg-score" class="value">-</div></div>
        <div class="stat"><div class="label">平均异常概率</div><div id="stat-avg-ap" class="value">-</div></div>
      </div>
      <div class="actions" style="justify-content:flex-end">
        <button id="refresh-stats" class="secondary">刷新统计</button>
      </div>
    </div>
  </div>

  <div class="grid" style="margin-top:12px">
    <div class="card">
      <h3>评估记录</h3>
      <table>
        <thead><tr><th>时间</th><th>评分</th><th>异常概率(%)</th></tr></thead>
        <tbody id="eval-tbody"></tbody>
      </table>
    </div>
    <div class="card">
      <h3>训练计划编辑</h3>
      <div class="field"><label>等级</label><select id="level"><option value="beginner">beginner</option><option value="intermediate">intermediate</option><option value="advanced">advanced</option></select></div>
      <div class="field"><label>次数 reps</label><input id="reps" type="number" min="1" max="30" /></div>
      <div class="field"><label>组数 sets</label><input id="sets" type="number" min="1" max="10" /></div>
      <div class="field"><label>休息秒数 rest_seconds</label><input id="rest" type="number" min="15" max="300" /></div>
      <div class="field"><label>动作列表（每行：名称,时长分钟,提示）</label><textarea id="exercises-text" rows="6"></textarea></div>
      <div class="actions" style="justify-content:flex-end"><button id="save-plan">保存计划</button></div>
    </div>
    </div>
  </div>
  <div class="grid" style="margin-top:12px">
    <div class="card">
      <h3>训练反馈</h3>
      <div class="field"><label>依从度(%)</label><input id="fb-adherence" type="number" min="0" max="100" /></div>
      <div class="field"><label>疼痛等级(0-10)</label><input id="fb-pain" type="number" min="0" max="10" /></div>
      <div class="field"><label>完成时间(分钟)</label><input id="fb-time" type="number" min="0" max="180" /></div>
      <div class="field"><label>备注</label><input id="fb-note" type="text" /></div>
      <div class="actions" style="justify-content:flex-end"><button id="send-feedback">提交反馈</button></div>
    </div>
    <div class="card">
      <h3>今日训练计划</h3>
      <div id="plan-view">加载中...</div>
    </div>
  </div>
  <div id="toast" class="toast"></div>
</main>
<script>
 const chartEl = document.getElementById('chart');
 const chartFallback = document.getElementById('chart-fallback');
 const chart = (window.echarts && typeof window.echarts.init === 'function') ? window.echarts.init(chartEl) : null;
 // Canvas chart fallback
 let chartCanvas = null;
 let chartCtx = null;
 let chartData = [];
 function ensureCanvas(){
   if(!chartCanvas){
     chartCanvas = document.createElement('canvas');
     chartCanvas.id = 'chart-canvas';
     chartCanvas.style.width = '100%';
     chartCanvas.style.height = '320px';
     chartEl.innerHTML = '';
     chartEl.appendChild(chartCanvas);
   }
   const dpr = window.devicePixelRatio || 1;
   const cssW = chartEl.clientWidth || 600;
   const cssH = chartEl.clientHeight || 320;
   chartCanvas.width = Math.max(300, Math.floor(cssW * dpr));
   chartCanvas.height = Math.floor(cssH * dpr);
   chartCtx = chartCanvas.getContext('2d');
   chartCtx.setTransform(1,0,0,1,0,0);
   chartCtx.scale(dpr, dpr);
 }
 function drawCanvasChart(items){
   chartData = items || [];
   ensureCanvas();
   const styles = getComputedStyle(document.documentElement);
   const border = (styles.getPropertyValue('--border') || '#e5e7eb').trim();
   const textColor = (styles.getPropertyValue('--text') || '#111827').trim();
   const scoreColor = (styles.getPropertyValue('--primary') || '#2563eb').trim();
   const anomalyColor = '#ef4444';
   const ctx = chartCtx;
   const W = chartEl.clientWidth || 600;
   const H = chartEl.clientHeight || 320;
   ctx.clearRect(0,0,W,H);
   const m = { left: 40, right: 12, top: 18, bottom: 26 };
   const iw = W - m.left - m.right;
   const ih = H - m.top - m.bottom;
   // axes
   ctx.strokeStyle = border; ctx.lineWidth = 1;
   ctx.beginPath(); ctx.moveTo(m.left, m.top); ctx.lineTo(m.left, m.top + ih); ctx.lineTo(m.left + iw, m.top + ih); ctx.stroke();
   // grid + y labels
   ctx.fillStyle = textColor; ctx.font = '12px system-ui, -apple-system, Segoe UI, Roboto';
   for(let y=0; y<=100; y+=20){
     const yy = m.top + ih * (1 - y/100);
     ctx.strokeStyle = border; ctx.beginPath(); ctx.moveTo(m.left, yy); ctx.lineTo(m.left + iw, yy); ctx.stroke();
     ctx.fillStyle = textColor; ctx.fillText(String(y), 8, yy+4);
   }
   const n = items.length;
   if(!n){ ctx.fillText('暂无评估数据', m.left + 8, m.top + 18); return; }
   const times = items.map(x => x.timestamp);
   const xs = (i) => m.left + (n<=1 ? iw/2 : (iw*(i/(n-1))));
   const yScore = (v) => m.top + ih * (1 - (Math.max(0, Math.min(100, v))/100));
   const yAnom = (v) => m.top + ih * (1 - (Math.max(0, Math.min(100, v))/100));
   // score line
   ctx.strokeStyle = scoreColor; ctx.lineWidth = 2; ctx.beginPath();
   items.forEach((it,i)=>{ const x=xs(i), y=yScore(it.score||0); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }); ctx.stroke();
   // anomaly line
   ctx.strokeStyle = anomalyColor; ctx.lineWidth = 2; ctx.beginPath();
   items.forEach((it,i)=>{ const x=xs(i), y=yAnom(Math.round((it.anomaly_prob||0)*100)); if(i===0) ctx.moveTo(x,y); else ctx.lineTo(x,y); }); ctx.stroke();
   // legend
   ctx.fillStyle = textColor; ctx.font = '12px system-ui, -apple-system, Segoe UI, Roboto';
   ctx.fillText('功能评分', m.left+18, m.top-2);
   ctx.fillText('异常概率(%)', m.left+96, m.top-2);
   ctx.fillStyle = scoreColor; ctx.beginPath(); ctx.arc(m.left+10, m.top-6, 4, 0, Math.PI*2); ctx.fill();
   ctx.fillStyle = anomalyColor; ctx.beginPath(); ctx.arc(m.left+88, m.top-6, 4, 0, Math.PI*2); ctx.fill();
   // x labels
   ctx.fillStyle = textColor; ctx.textAlign = 'center';
   const tickCount = Math.min(6, n);
   for(let i=0;i<tickCount;i++){
     const idx = Math.round(i*(n-1)/(tickCount-1));
     const x = xs(idx);
     const d = new Date(times[idx]);
     const lab = isNaN(d.getTime()) ? String(idx+1) : `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;
     ctx.fillText(lab, x, m.top + ih + 18);
   }
   ctx.textAlign = 'start';
 }
 window.addEventListener('resize', () => { if(chartData && chartData.length) drawCanvasChart(chartData); });
 async function fetchStats(){
  const res = await fetch('/api/stats');
  const data = await res.json();
  document.getElementById('stat-count').textContent = data.count;
  document.getElementById('stat-avg-score').textContent = (data.avg_score ?? 0).toFixed(2);
  document.getElementById('stat-avg-ap').textContent = Math.round((data.avg_anomaly_prob ?? 0)*100) + '%';
}

function renderEvaluationsTable(items){
  const tbody = document.getElementById('eval-tbody');
  tbody.innerHTML = '';
  items.slice(-10).forEach(i => {
    const tr = document.createElement('tr');
    tr.innerHTML = `<td>${i.timestamp}</td><td>${i.score}</td><td>${Math.round(i.anomaly_prob*100)}</td>`;
    tbody.appendChild(tr);
  });
}

async function fetchEvaluations() {
  const res = await fetch('/api/evaluations');
  const data = await res.json();
  const items = data.items || [];
  const times = items.map(x => x.timestamp);
  const scores = items.map(x => x.score);
  const anomalies = items.map(x => Math.round(x.anomaly_prob * 100));
  if (chart && typeof chart.setOption === 'function') {
    chart.setOption({
      tooltip: { trigger: 'axis' },
      legend: { data: ['功能评分', '异常概率(%)'] },
      xAxis: { type: 'category', data: times },
      yAxis: [ { type: 'value', min: 0, max: 100 }, { type: 'value', min: 0, max: 100 } ],
      series: [
        { name: '功能评分', type: 'line', data: scores, smooth: true },
        { name: '异常概率(%)', type: 'line', data: anomalies, yAxisIndex: 1, smooth: true }
      ]
    });
  } else {
    drawCanvasChart(items);
    chartFallback.textContent = '';
  }
  renderEvaluationsTable(items);
}

function renderPlanView(plan){
  const el = document.getElementById('plan-view');
  if(!plan){ el.innerHTML = '<em>暂无计划，请先模拟一次数据。</em>'; return; }
  const levelMap = { beginner: '初级', intermediate: '中级', advanced: '高级' };
  const level = levelMap[plan.level] || plan.level || '中级';
  const reps = plan.reps ?? 10;
  const sets = plan.sets ?? 3;
  const rest = plan.rest_seconds ?? 60;
  const exercises = (plan.exercises || []).map(x => {
    const name = x.name || '动作';
    const dur = x.duration_min ?? 5;
    const cue = x.cue || '';
    return `<li>${name} — ${dur}分钟${cue ? '（提示：'+cue+'）' : ''}</li>`;
  }).join('');
  el.innerHTML = `
    <div>等级：${level}；次数：${reps}；组数：${sets}；休息：${rest}秒</div>
    <div style="margin-top:6px">动作列表：</div>
    <ul style="margin:6px 0 0 18px">${exercises || '<li>无</li>'}</ul>
  `;
}

function renderPlanEditor(plan){
  if(!plan){ return; }
  document.getElementById('level').value = plan.level || 'intermediate';
  document.getElementById('reps').value = plan.reps || 10;
  document.getElementById('sets').value = plan.sets || 3;
  document.getElementById('rest').value = plan.rest_seconds || 60;
  const lines = (plan.exercises || []).map(e => `${e.name},${e.duration_min || 5},${e.cue || ''}`);
  document.getElementById('exercises-text').value = lines.join('\n');
}

async function fetchPlan() {
  const res = await fetch('/api/plan');
  if (res.ok) {
    const data = await res.json();
    renderPlanView(data.plan);
    renderPlanEditor(data.plan);
  } else {
    document.getElementById('plan-view').innerHTML = '<em>暂无计划，请先模拟一次数据。</em>';
    renderPlanEditor(null);
  }
}

function parseExercises(text){
  return text.split('\n').map(l => l.trim()).filter(Boolean).map(l => {
    const parts = l.split(',');
    return {
      name: (parts[0]||'动作').trim(),
      duration_min: parseInt(parts[1]||'5', 10),
      cue: (parts[2]||'').trim()
    };
  });
}

async function savePlan(){
  const plan = {
    level: document.getElementById('level').value,
    reps: parseInt(document.getElementById('reps').value||'10', 10),
    sets: parseInt(document.getElementById('sets').value||'3', 10),
    rest_seconds: parseInt(document.getElementById('rest').value||'60', 10),
    exercises: parseExercises(document.getElementById('exercises-text').value||'')
  };
  const res = await fetch('/api/plan/update', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(plan) });
  if(res.ok){ toast('已保存计划'); }
  await fetchPlan();
}

async function sendFeedback(){
  const payload = {
    adherence: parseInt(document.getElementById('fb-adherence').value||'0', 10),
    pain: parseInt(document.getElementById('fb-pain').value||'0', 10),
    duration_minutes: parseInt(document.getElementById('fb-time').value||'0', 10),
    note: document.getElementById('fb-note').value||''
  };
  try{
    const res = await fetch('/api/feedback', { method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify(payload) });
    if(res.ok){ toast('已提交反馈'); }
    else { toast('提交失败'); }
  }catch(e){ toast('提交失败'); }
}

function toast(msg){
  const el = document.getElementById('toast');
  el.textContent = msg;
  el.classList.add('show');
  setTimeout(() => { el.classList.remove('show'); }, 1500);
}

// 模拟一次数据
async function simulateOnce(){
  const fs = 100; // 采样率 Hz
  const durationSeconds = 2;
  const samples = fs * durationSeconds;
  const imu = [];
  const pressure = [];
  for(let i=0;i<samples;i++){
    const t = i / fs;
    const acc = [
      Math.sin(2*Math.PI*0.5*t) + 0.05*Math.random(),
      Math.sin(2*Math.PI*0.6*t) + 0.05*Math.random(),
      Math.sin(2*Math.PI*0.4*t) + 0.05*Math.random()
    ];
    const gyro = [
      0.5*Math.sin(2*Math.PI*0.3*t) + 0.02*Math.random(),
      0.4*Math.sin(2*Math.PI*0.2*t) + 0.02*Math.random(),
      0.3*Math.sin(2*Math.PI*0.25*t) + 0.02*Math.random()
    ];
    const mag = [
      0.1*Math.sin(2*Math.PI*0.1*t) + 0.01*Math.random(),
      0.1*Math.sin(2*Math.PI*0.12*t) + 0.01*Math.random(),
      0.1*Math.sin(2*Math.PI*0.08*t) + 0.01*Math.random()
    ];
    imu.push({ acc, gyro, mag });
    pressure.push(0.5 + 0.2*Math.sin(2*Math.PI*0.5*t) + 0.05*Math.random());
  }
  const res = await fetch('/api/receive_sensor_data', {
    method: 'POST', headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ imu, pressure, fs })
  });
  if(res.ok){ toast('已模拟一次数据'); }
  await fetchEvaluations();
  await fetchPlan();
  await fetchStats();
}

document.getElementById('simulate').addEventListener('click', simulateOnce);
// 主题切换
const savedTheme = localStorage.getItem('theme');
if(savedTheme === 'dark'){ document.documentElement.classList.add('dark'); }
document.getElementById('toggle-theme').addEventListener('click', () => {
  const isDark = document.documentElement.classList.toggle('dark');
  localStorage.setItem('theme', isDark ? 'dark' : 'light');
});

document.getElementById('refresh').addEventListener('click', async () => { await fetchEvaluations(); await fetchPlan(); await fetchStats(); });
document.getElementById('save-plan').addEventListener('click', savePlan);
document.getElementById('send-feedback').addEventListener('click', sendFeedback);
document.getElementById('refresh-stats').addEventListener('click', fetchStats);

fetchEvaluations();
fetchPlan();
fetchStats();
</script>
</body>
</html>